[FE102] 前端必備:JavaScript (下)


Posted by s103071049 on 2021-05-26

JavaScript 網頁事件處理

一、eventListener 與 callback function

目標:學習怎麼對事件做出反應。事件很多,例如:捲動(scroll), 按按鍵(keyDown)

eventListener (事件監聽)

首先要先選到元素,我們這邊選 block,接著 element.addEventListener(監聽事件名稱, 函數)。最後重新整理,跑一次 => 點 yo hello~ => 跳出 click

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
  </head>
<body>
  <div id='block'>
    yo
    <a>hello~</a>
  </div>
  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click',onClick)

    function onClick() {
      alert('click!')
    }
  </script>
</body>
</html>

整段代碼:我在 element 上新增 EventListener 他叫 click (我想監聽的事件叫做 click),當有 click 這個事件發生時,瀏覽器幫我觸發 onClick 這個函式。這個函式我們叫他 callback function。在這個 function 中可以做任何想做的事情。

大多數會偷懶直接用匿名函式寫,匿名函式和 callback function 一點關係都沒有,他只是沒有名稱。

  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', function() {
      alert('click!')
    })
  </script>

結論:可以對一個元素進行監聽,監聽什麼樣的事件,一旦這個事件被觸發,就會執行到後面的 function。

二、詳細講解 callback function

callback function(回呼函式)

Recall:監聽事件要先建立事件監聽器,EventListener(),第一個傳要監聽的事件名稱,第二個傳一個 function。

當有人點按鈕時,幫我呼叫這個 function。也就是當有人觸發監聽事件,你再告訴我,我就不用一直等在那邊,我可以去做其他事情,一旦這個事件發生,就呼叫這個 function。

  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', function() {
      alert('click!')
    })
  </script>

為甚麼要這樣寫? 你不知道使用者甚麼時候要按按鈕,如果按照下列寫法,程式會一直卡在那邊等待事件觸發。

  <script>
    const element = document.querySelector('#block')
    const event = element.addEventListener('click')
    function onClick() {
      alert('click!')
    }
  </script>

三、event(e) 是什麼碗糕?

callback function 可以拿到更多資訊。瀏覽器在乎叫 function 會拿進參數,參數可以隨意命名,通常命名 event 或 e。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
  </head>
<body>
  <div id='block'>
    yo
    <a>hello~</a>
  </div>
  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', function(e) {
      console.log(e)
    })
  </script>
</body>
</html>

也可以是這樣的寫法

  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', onClick)

    function onClick(e) {
      console.log(e)
    }
  </script>

e.target 表示我點了哪個元素。所以,可以用 e.target 取得我到底點了哪個元素。

  <script>
    const element = document.querySelector('#block')
    element.addEventListener('click', function(e) {
      console.log(e.target)
    })
  </script>

以瀏覽器的角度,當我點了這個 element,瀏覽器會幫我呼叫這個 function,然後 e 會帶一些資訊進去。所以後面的 e 是瀏覽器帶給我的資訊。

<body>
  <div id='block'>
    yo
    <a>hello~</a>
  </div>
  <input/>
  <script>
    const element = document.querySelector('input')
    element.addEventListener('keydown', function (e) {
      console.log(e.key)
    })
  </script>
</body>

按按鈕切換背景顏色

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>
      .active {
        background: red;
      }
    </style>
  </head>
<body>
  <div id='block'>
    yo
    <a>hello~</a>
    <button class='change-btn'>change </button>
  </div>
  <script>
    const element = document.querySelector('.change-btn')
    element.addEventListener('click', function (e) {
      document.querySelector('body').classList.toggle('active')
    })
  </script>
</body>
</html>

e 是瀏覽器幫我傳過來的變數,裡面會有和我事件相關的資訊。

四、表單事件處理 onSubmit

可以在表單上加上 submit 這個事件,在表單送出以前就會觸發這個事件。可以對這個事件做一些處理,例如 : 表單不要送出,通常會用在表單驗證。

表單如果沒有指定,他預設的網頁會是同個網頁,method 會是 get。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>

    </style>
  </head>
<body>
  <form class='login-form' method="GET" action="">
    <div>
      username: <input name="username"/>
    </div>
    <div>
      password: <input name="password" type="password"/>
    </div>
    <div>
      password again: <input name="password2" type="password"/>
    </div>
    <input type="submit"/>      
  </form>
  <script>
    const element = document.querySelector('.login-form')
    element.addEventListener('submit', function (e) {
      alert("submit")
    })
  </script>
</body>
</html>

e.preventDefault() 表示阻止他預設的行為,表單的預設行為=submit,preventDefault() 是 e 裡面的一個函式。
所以現在就不送出了。

  <script>
    const element = document.querySelector('.login-form')
    element.addEventListener('submit', function(e) {
      e.preventDefault()
    })
  </script>

如何拿值,透過 input1.value

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>

    </style>
  </head>
<body>
  <form class='login-form' method="GET" action="">
    <div>
      username: <input name="username"/>
    </div>
    <div>
      password: <input name="password" type="password"/>
    </div>
    <div>
      password again: <input name="password2" type="password"/>
    </div>
    <input type="submit"/>      
  </form>
  <script>
    const element = document.querySelector('.login-form')
    element.addEventListener('submit', function(e) {
      const input1 = document.querySelector('input[name=password]')
      const input2 = document.querySelector('input[name=password2]')
      if (input1.value !== input2.value) {
        alert('密碼不同')
        e.preventDefault()
      }

    })
  </script>
</body>
</html>

五、阻止預設行為:preventDefault

阻止瀏覽器的預設行為 e.preventDefault,常見用法:超連結、表單驗證。

超連結的預設是按了之後點到某個地方,所以加了 e.preventDefault 不管怎麼點他都不會有反應。

    <a href="https://www.google.com/">link</a>      
  </form>
  <script>
    const element = document.querySelector('a')
    element.addEventListener('click', function(e) {
      e.preventDefault()
    })

  </script>

這樣寫,表示我沒辦法打 e 這個字。

<body>
  <form class='login-form' method="GET" action="">
    <div>
      username: <input name="username"/>
    </div>
    <div>
      password: <input name="password" type="password"/>
    </div>
    <div>
      password again: <input name="password2" type="password"/>
    </div>
    <input type="submit"/>      
  </form>
  <script>
    const element = document.querySelector('input[name=username]')
    element.addEventListener('keypress', function(e) {
      if (e.key === 'e') {
        e.preventDefault()
      }
    })

  </script>
</body>

六、事件傳遞機制

甚麼是事件傳遞機制

前言:
因為他們是有層層疊疊的關係,所以點綠色區塊,也會觸發到紅色區塊的東西。所以會先從點到最近的那個開始,然後慢慢擴散出去,像漣漪一樣。這個東西就是瀏覽器的事件傳遞機制。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>
      .outer {
        width: 500px;
        height: 200px;
        background: red;
      }

      .inner {
        width: 300px;
        height: 100px;
        background: green;
      }

    </style>
  </head>
<body>
  <div class='outer'>
    <div class='inner'>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')


    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className)
      })
    }
  </script>
</body>
</html>

事件傳遞機制詳解:捕獲與冒泡

先捕獲再冒泡。

如何將 eventListener 掛在不同的階段 ? A:.addEventListener() 放第三個參數-布林,true:捕獲階段、false:冒泡階段(預設為 flase)。

點 click me => 發現先捕獲,再冒泡。

.outer 捕獲
.inner 捕獲
.btn 捕獲
.btn 冒泡
.inner 冒泡
.outer 冒泡

<body>
  <div class='outer'>
    <div class='inner'>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')


    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '捕獲')
      },true)

      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      },false)
    }
  </script>
</body>

target phase 會依據你先放哪個 EventListener 而是哪個 EventListener。
ex:我們在觸發 clickme 他是處於 target phase
.outer 捕獲
.inner 捕獲
.btn 冒泡
.btn 捕獲
.inner 冒泡
.outer 冒泡

  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')


    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      },false)

      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '捕獲')
      },true)


    }
  </script>

放一個監聽在捕獲階段,就可以記錄到所有按鈕的事件。

<body>
  <div class='outer'>
    <div class='inner'>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')

    window.addEventListener('click', function(e) {
      console.log(e)
    }, true)

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      },false)
    }

  </script>
</body>

超連結點下去會完全沒有用,一旦 call 了 e.preventDefault(),這個 e.preventDefault() 會沿路傳下去。所以儘管我們是在這邊加上的,傳到 a 還是會有效果,因此整個頁面的表單送出、超連結都不會有效果。

<body>
  <div class='outer'>
    <div class='inner'>
      <a href='./test'>click</a>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')
    addEvent('.btn')

    window.addEventListener('click', function(e) {
      e.preventDefault()
    }, true)

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      },false)
    }

  </script>
</body>

七、別向上級回報:stopPropagation

冒泡的過程就像逐級向上回報

當我們點按鈕時,只會觸發按鈕的事件;點綠色的地方,綠色的仍會傳遞。

stopPropagation() 會阻止這個事件繼續傳遞。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>
      .outer {
        width: 500px;
        height: 200px;
        background: red;
      }

      .inner {
        width: 300px;
        height: 100px;
        background: green;
      }

    </style>
  </head>
<body>
  <div class='outer'>
    <div class='inner'>
      <a href='./test'>click</a>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn 冒泡')
      })

  </script>
</body>
</html>

發現,不管怎麼點都不會有 log,連剛剛的 EventListener 都不會觸發。這是因為你在 windows 這個階段就已經阻止他傳下去,所以下面的東西都不會收到事件。

<body>
  <div class='outer'>
    <div class='inner'>
      <a href='./test'>click</a>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')

    window.addEventListener('click', function(e) {
      e.stopPropagation()
    },true)

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn 冒泡')
      })

  </script>
</body>

一個按鈕可以加兩個 eventListener() 是沒有問題。兩個 eventListener() 都會被觸發。

 <script>
    addEvent('.outer')
    addEvent('.inner')

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        console.log('.btn click 1')
      })

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        console.log('.btn click 2')
      })

  </script>

加上 e.stopPropagation(),阻止冒泡,但兩個事件還是會被觸發。因為這兩個事件都在 target phase,所以雖然阻止事件傳遞原本的還是會被觸發。

<body>
  <div class='outer'>
    <div class='inner'>
      <a href='./test'>click</a>
      <button class='btn'>click me </button>
    </div>
  </div>
  <script>
    addEvent('.outer')
    addEvent('.inner')

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn click 1')
      })

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn click 2')
      })

  </script>
</body>

加上 e.stopImmediatePropagation() 就會阻止其他任何的 EventListener => 只會觸發第一個。會當前立刻阻止所有事件傳遞。

  <script>
    addEvent('.outer')
    addEvent('.inner')

    function addEvent(className) {
      document.querySelector(className)
      .addEventListener('click', function() {
        console.log(className, '冒泡')
      })
    }

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopImmediatePropagation()
        console.log('.btn click 1')
      })

    document.querySelector('.btn')
      .addEventListener('click', function(e) {
        e.stopPropagation()
        console.log('.btn click 2')
      })

  </script>

八、新手 100% 會搞錯的事件機制問題

首先,我們有兩個按鈕,class 都叫 btn,

<!DOCTYPE html>
<html>
  <head>
    <meta charset="urf-8"> 
    <title>i am title</title>
    <link rel="stylesheet" href="./style.css"/>
    <style>
    </style>
  </head>
<body>
  <div class='outer'>
    <button class='btn'>1</button>
    <button class='btn'>2</button>
  </div>
  <script>
    document.querySelector('.btn')
    .addEventListener('click',function() {

    })
  </script>
</body>
</html>

現在幫 .btn 加 EventListener,點了就是 1 => 點了 btn1 出現 1,但點了 btn2 沒反應。這是因為 document.querySelector 只會回傳第一個元素。也就是說,我們只有幫第一個元素加 EventListener。

    document.querySelector('.btn')
    .addEventListener('click',function() {
      alert(1)
    })

如果想幫每一個都加,使用 querySelectorAll => TypeError: document.querySelectorAll(...).addEventListener is not a function
這是因為 querySelectorAll 回傳的是一個 list。

    document.querySelectorAll('.btn')
    .addEventListener('click',function() {
      alert(1)
    })

修改成以下形式:發現不管點哪個 btn 都是 6
=> 這是因為 scope

<body>
  <div class='outer'>
    <button class='btn'>1</button>
    <button class='btn'>2</button>
    <button class='btn'>3</button>
    <button class='btn'>4</button>
    <button class='btn'>5</button>
  </div>
  <script>
    const elements = document.querySelectorAll('.btn')
    for (var i=0; i<elements.length; i++) {
      elements[i].addEventListener('click',function() {
        alert(i+1)
        })    
    }

  </script>
</body>

這個 function 是在我點擊那刻觸發,點擊那刻 i 的值是 elements.length ,所以迴圈跑完時 i = 5,所以當我點擊時 alert(i+1) = 6

  <script>
    const elements = document.querySelectorAll('.btn')
    for (var i=0; i<elements.length; i++) {
      elements[i].addEventListener('click',function() {
        alert(i+1)
        })    
    }
    alert(i)
  </script>

所以可以如何修正 ? (1) 用 let (2) 新增 data-value,以屬性的方式拿取。

  <div class='outer'>
    <button class='btn' data-value='1'>1</button>
    <button class='btn' data-value='2'>2</button>
    <button class='btn' data-value='3'>3</button>
    <button class='btn' data-value='4'>4</button>
    <button class='btn' data-value='5'>5</button>
  </div>
  <script>
    const elements = document.querySelectorAll('.btn')
    for (var i=0; i<elements.length; i++) {
      elements[i].addEventListener('click',function(e) {
        console.log(e.target.getAttribute('data-value'))
        })    
    }
  </script>

現在,我們想新增按鈕。

<body>
  <div class='outer'>
    <button class='add-btn'>add</button>
    <button class='btn' data-value='1'>1</button>
    <button class='btn' data-value='2'>2</button>
  </div>
  <script>
    let num=3
    const elements = document.querySelectorAll('.btn')
    for (var i=0; i<elements.length; i++) {
      elements[i].addEventListener('click',function(e) {
        alert(e.target.getAttribute('data-value'))
        })    
    }

    document.querySelector('.add-btn').addEventListener('click',       function() {
      const btn = document.createElement('button')
      btn.setAttribute('data-value', num)
      btn.innerText = num
      num++
      document.querySelector('.outer').appendChild(btn)
    })
  </script>
</body>

為甚麼點新增的按鈕,沒有反應 ?
當我在跑 querySelectorAll => 他的 element 也只有 1, 2 兩個按鈕,動態新增後才有新的按鈕,但動態新增的部分並未幫他們加 EventListener。所以他點了就不會有反應。

整理:

  1. 點擊產生事件,會從上傳到下,再從下傳到上。事件機制是不管如何都會發生。不管有沒有加監聽器,事件都會這樣傳。加了監聽器表示在傳遞時,Listener 就可以監聽到這個事件,拿到這個事件,然後我就可以決定怎麼處理。更詳細地說,事件不管怎樣都會傳,我加上監聽器去攔截事件,拿到事件決定對事件如何處理,再繼續傳遞下去或不傳遞。
  2. e.stopPropagation() 阻止事件傳遞,後面的連結會斷掉。所以我後續的 node 就收不到這個事件。
  3. 第三個參數,true/ false 是決定事件要加在哪個 phase
  4. 點到元素本身是 target phase,這時觸發的順序依照先捕獲後冒泡依序傳遞。

九、欸等等幫我拿餐點:event delegation

如果有一百個按鈕,就要加一百個 EventListener ? 有一千個按鈕就要加一千個 EventListener ? 將性質相似的每個元素加上其專屬的 EventListener 這樣顯然很沒效率。

所以怎麼辦呢 ?

我們可以利用事件冒泡的性質,每一個 click event 都會冒泡到 outer 去。所以在 outer 上加 EventListener 就好了。就可以處理下面所有的 btn,動態新增的也可以。

這就叫做 event delegation 事件代理。白話的說,一群人去麥當勞點餐,不可能所有人都在下面等,有一兩個在樓下幫忙取餐,其他人先占位子。那個人就負責拿所有人的餐點,現在這個 outer 就是那個幫大家拿餐點的人。

本來事件就會一直冒泡。


<body>
  <div class='outer'>
    <button class='add-btn'>add</button>
    <button class='btn' data-value='1'>1</button>
    <button class='btn' data-value='2'>2</button>
  </div>
  <script>
    let num=3

    document.querySelector('.add-btn').addEventListener('click', function() {
      const btn = document.createElement('button')
      btn.setAttribute('data-value', num)
      btn.innerText = num
      btn.classList.add('btn')
      num++
      document.querySelector('.outer').appendChild(btn)
    })

    document.querySelector('.outer').addEventListener('click', function(e) {
      if (e.target.classList.contains('btn')) {
        alert(e.target.getAttribute('data-value'))
      }
    })
  </script>
</body>

事件代理機制的優點:
(1) 有效率,不用浪費資源建立無數個做相同事件的 function。只要用一個 EventListener 就可以管理處理這些事情。
(2) 處理動態新增的信息。透過冒泡機制,讓底下就算是新增的東西,一樣可以接到他的事件。










Related Posts

CS 50 Dynamic Memory Allocation

CS 50 Dynamic Memory Allocation

Day 93

Day 93

CS50 HTTP (Hypertext Transfer Protocol)

CS50 HTTP (Hypertext Transfer Protocol)


Comments